package com.dbflow5.paging;

import com.dbflow5.config.DBFlowDatabase;
import com.dbflow5.config.FlowManager;
import com.dbflow5.database.DatabaseWrapper;
import com.dbflow5.observing.OnTableChangedObserver;
import com.dbflow5.observing.TableObserver;
import com.dbflow5.query.*;
import com.dbflow5.transaction.Transaction;

import java.util.*;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;

public class QueryDataSource<T, TQuery extends Transformable<T> & ModelQueriable<T>> extends PositionalDataSource<T> {
    private final TQuery transformable;
    private final DBFlowDatabase database;

    public QueryDataSource(TQuery transformable, DBFlowDatabase database) {
        this.transformable = transformable;
        this.database = database;

        if (transformable instanceof WhereBase<?> && !(((WhereBase<?>) transformable).queryBuilderBase() instanceof Select)) {
            throw new IllegalArgumentException("Cannot pass a non-SELECT cursor into this data source.");
        }

        DBFlowDatabase db = FlowManager.getDatabaseForTable(associatedTables().stream().findFirst().get());
        // force initialize the db
        db.getWritableDatabase();

        TableObserver observer = db.tableObserver();
        // From could be part of many joins, so we register for all affected tables here.
        observer.addOnTableChangedObserver(onTableChangedObserver());
    }

    private Set<Class<?>> associatedTables() {
        Set<Class<?>> classSet = null;
        From<T> from = transformable.extractFrom();
        if(from != null){
            classSet = from.associatedTables();
        }
        if(classSet == null){
            classSet = Collections.singleton(transformable.table());
        }

        return classSet;
    }

    private OnTableChangedObserver onTableChangedObserver(){
        return new OnTableChangedObserver(new ArrayList<>(associatedTables())) {
            @Override
            protected void onChanged(Set<Class<?>> tables) {
                if (!tables.isEmpty()) {
                    invalidate();
                }
            }
        };
    }

    @Override
    public void loadRange(LoadRangeParams params, LoadRangeCallback<T> callback) {
        database.beginTransactionAsync((Function<DatabaseWrapper, List<T>>) db -> transformable.constrain(params.startPosition, params.loadSize).queryList(db)).execute(null, null, null, (listTransaction, ts) -> {
            callback.onResult(ts);
            return null;
        });
    }

    @Override
    public void loadInitial(LoadInitialParams params, LoadInitialCallback<T> callback) {
        database.beginTransactionAsync((Function<DatabaseWrapper, Long>) db -> SQLite.selectCountOf().from(transformable).longValue(db)).execute(null, null, null, (objectTransaction, count) -> {
            final int max;
            if(params.requestedLoadSize >= count - 1) {
                max = count.intValue();
            }else {
                max = params.requestedLoadSize;
            }
            database.beginTransactionAsync((Function<DatabaseWrapper, List<T>>) db -> transformable.constrain(params.requestedStartPosition, max).queryList(db)).execute(null, null, null, (listTransaction, ts) -> {
                callback.onResult(ts, params.requestedStartPosition, count.intValue());
                return null;
            });
            return null;
        });

    }

    static class Factory<T, TQuery extends Transformable<T> & ModelQueriable<T>> extends DataSource.Factory<Integer, T> {
        private final TQuery transformable;
        private final DBFlowDatabase database;

        public Factory(TQuery transformable, DBFlowDatabase database) {
            this.transformable = transformable;
            this.database = database;
        }

        @Override
        public DataSource<Integer, T> create() {
            return new QueryDataSource<>(transformable, database);
        }
    }

    public static <T, TQuery extends Transformable<T> & ModelQueriable<T>> Factory<T, TQuery> newFactory(TQuery transformable, DBFlowDatabase database) {
        return new Factory<>(transformable, database);
    }

    public static <T, TQuery extends Transformable<T> & ModelQueriable<T>> Factory<T, TQuery> toDataSourceFactory(TQuery query, DBFlowDatabase database) {
        return newFactory(query, database);
    }
}

